#!/usr/local/bin/python2.7

from xml.etree.ElementTree import XML
from zipfile import ZipFile, is_zipfile

import os, shutil, string, difflib

from component import *
from errors import *
from constants import *
from tagdict import *

class Parser(object):
    """
    Reads and parses a properly formatted docx file into a hierarchical 
    Element object

    The parser output is a component that contains an
    xml.etree.ElementTree.Element object with the plant component data in
    a form that reflects its natural hierarchical structure.

    Ideally, the *pic_path* and *db_root_path* instance attributes should
    be absolute paths.  The *filepath* property should be a relative path
    that starts at the *db_root_path* directory.
    """

    def __init__(self, db_root, filepath = None):
        """
        *db_root* is the absolute path to the docx database root directory.
        Note that this value can change from machine to machine depending on
        how the database is mounted.

        *filepath* is the relative path from the database root directory to
        the docx file that will be parsed.  Note that *filepath* must be 
        specified before the docx file can be processed.
        """

        # filepath to the file of this instances actual component
        self._filepath = filepath
        self.temp_path = Constants.temp_files_dir
        self.pic_path = Constants.pics_dir
        self.db_root_path = db_root
        self._nonfatal_errors = []

        self._pic_count = 1

        # path through docx zip structure to doc's main xml data
        self._datapath = 'word/document.xml'

        # stores the namespace of the document.xml file
        self._namespace = ''

        # used to build path through docx xml to all table rows
        self._docxtagpath = []
        self._docxtagpath.append('body')
        self._docxtagpath.append('tbl')
        self._docxtagpath.append('tr')
        self._nav_to_table = ''

        self._nav_in_table = {}
        self._nav_in_table['cell'] = 'tc'
        self._nav_in_table['paragraph'] = 'p'
        self._nav_in_table['run'] = 'r'
        self._nav_in_table['text'] = 't'
        self._nav_in_table['smarttag'] = 'smartTag'

        # a list of row Elements from the docx table to iterate through
        self._rows = None

        # holds completed element structure ready to write to xml
        self._component = Component()

        self._tagdict_filepath = Constants.tagdict_filepath
        self._tag_dict = None
        self._prepareTagDict()
        self._nomatch_key = 'nomatches'
        self._newsection_key = 'newsection'

    def _prepareTagDict(self):
        os.chdir(os.path.dirname(os.path.abspath(__file__)))
        tag_dict = TagDict()
        tag_dict.filepath = self._tagdict_filepath
        tag_dict.load()
        self._tag_dict = tag_dict.map

    def processFile(self):
        """
        Parse and process the docx file specified by the *filepath*
        attribute into a *Component* instance that results in a complete
        xml Element structure._pics
        """

        self._validateForProcessing()
        self._loadRows()
        self._buildElemStructure()
        self._indexPics()
        self._addPaths()

    def _validateForProcessing(self):
        """
        Checks filepaths, filetypes, etc. are correct/valid before parsing.
        Exceptions are raised for errors.
        """

        if self._filepath is None:
            raise NoFilepathError()
        elif not os.path.exists(self._filepath):
            raise MissingFileError(self._filepath)
        elif not is_zipfile(self._filepath):
            raise NotZipFileError(self._filepath)
        elif os.path.exists(self.temp_path):
            shutil.rmtree(self.temp_path) # delete temp zip extracted files
            if os.path.exists(self.temp_path):
                raise TempDirExistsError(self.temp_path, self._filepath)

        # create directory if it doesn't exist
        if not os.path.exists(self.pic_path):
            os.makedirs(self.pic_path)

    def _loadRows(self):
        """
        Uses known MS Word docx xml file structure to extract table rows
        containing the plant component information.
        """

        # open the file and extract the xml content into an etree Element
        with ZipFile(self._filepath, 'r') as myzip:
            elem = XML(myzip.open(self._datapath, 'r').read())
            myzip.extractall(self.temp_path)

        # locate the table and return a list of row Elements
        index = elem.tag.find('}')
        self._namespace = elem.tag[:index + 1]

        # extract the rows into a list of xml elements
        self._buildXMLNavPaths()
        self._rows = elem.findall(self._nav_to_table)

    def _buildXMLNavPaths(self):
        # must occur after _namespace has been set in _loadrows()
        for tag in self._docxtagpath:
            self._nav_to_table += self._makeNamespacePath(tag + '/')
        self._nav_to_table = self._nav_to_table[:-1]

        for key, val in self._nav_in_table.items():
            self._nav_in_table[key] = self._makeNamespacePath(val)
        
    def _makeNamespacePath(self, short_path):
        return self._namespace + short_path

    def _buildElemStructure(self):
        """
        Iterates through each docx row xml element and strips out 
        information from the table cells and stores it in an xml hierarchy.

        The first cell in each row becomes an xml element tag.  All
        following cells in the row become sub elements of it the with a 
        'datum' tag - almost in a key-value dictionary pattern.  This
        method is the meat of the *Parser* class.
        """

        # iterate through table rows to pull out component data
        for row in self._rows:
            rowcell_items = self._extractCells(row)

            # convert extracted data into plant component field
            raw_name = rowcell_items.pop(0)
            if len(raw_name) == 0:
                continue

            field_name = self._get_match(raw_name)
            if field_name == 'NoMatches':
                self._component.addNoMatch(raw_name, rowcell_items)
            elif self.isCategory(raw_name):
                self._component.setCurrentDict(field_name)
            else:
                vals = []
                for field_value in rowcell_items:
                    field_value = self._fixEncoding(field_value)
                    field_value = self._normalizeText(field_value)
                    if len(field_value) > 0:
                        vals.append(field_value)
                self._component.addField(field_name, vals)

    def _get_match(self, mystring, match_limit = .7):
        """
        Determines which xml element tag/key the passed *mystring* matches.

        *mystring* is the string (from the docx file) that will be matched
        to a standardized tag/key list using the difflib library.
        *mystring* must match a standard key with a score of *match_limit*
        or better where *match_limit* is between 0 and 1.

        Used to match fields in the word document to expected field names
        even if the word doc field name has been disturbed/tampered with.
        This allows the final component xml files to all contain a
        standardized set of xml tags for component information.
        """

        matches = difflib.get_close_matches(mystring, \
                               self._tag_dict.keys(), 1, match_limit)
        if len(matches) > 0:
            tag = self._tag_dict[matches[0]]
        else: tag = self._tag_dict[self._nomatch_key]
        return tag

    def isCategory(self, raw_name):
        index = raw_name.find(self._tag_dict[self._newsection_key])
        return (index > -1)

    def _fixEncoding(self, text):
        text = text.encode('cp1252', 'replace')
        text = text.replace('\x97', '-')
        text = ''.join(s for s in text if s in string.printable)
        text = text.decode('UTF-8', 'replace')

        return text

    def _extractCells(self, row):
        # build list with one entry for each word-doc table cell
        rowcell_items = []
        for cell in row.findall(self._nav_in_table['cell']):
            content = ''
            for paragraph in cell.findall(self._nav_in_table['paragraph']):
                for run in paragraph.findall(self._nav_in_table['run']):
                    for typing in run.findall(self._nav_in_table['text']):
                        content += typing.text

                # pull out data from screwy smartTags
                for smtag in paragraph.findall(self._nav_in_table['smarttag']):
                    content += self._extractSmartTagText(smtag)

                content += '\r'

            content = content.strip()
            rowcell_items.append(content)

        return rowcell_items

    def _extractSmartTagText(self, smarttag):
        content = ''

        for run in smarttag.findall(self._nav_in_table['run']):
            for typing in run.findall(self._nav_in_table['text']):
                content += typing.text

        for sub_smtag in smarttag.findall(self._nav_in_table['smarttag']):
            content += self._extractSmartTagText(sub_smtag)

        return content

    def _normalizeText(self, text):
        text = text.strip()
        text = text.lower()
        return text

    def _indexPics(self):
        """
        Pictures are extracted from the docx file and renamed with
        arbitrary names.  Paths and filenames for these extracted pictures
        are stored as sub elements in the hierarchy that describes this
        plant component.
        """

        picdir = 'word/media/' # location of pics in unzipped archive
        picpath = os.path.join(self.temp_path, picdir)

        if os.path.exists(picpath):
            files = os.listdir(picpath)
            for filename in files:
                newname = self._makePicName(filename)

                # add the pic to the component
                pic = Pic(newname, self.pic_path)
                self._component.addPic(pic)

                # copy and rename the pic
                src = os.path.join(self.temp_path, picdir, filename)
                dst = os.path.join(self.pic_path, newname)
                try:
                    shutil.copy2(src, dst)
                except Exception as error:
                    myerr = PictureCopyError(self._filepath, src, error)
                    self._nonfatal_errors.append(myerr)

        shutil.rmtree(self.temp_path) # delete temp zip extracted files

    def _makePicName(self, curr_name):
        base, ext = os.path.splitext(curr_name)
        new_base = self._component.name + '_' + str(self._pic_count)
        pic_name = new_base + ext
        self._pic_count += 1

        return pic_name

    def _addPaths(self):
        self._filepath = self._filepath.replace('\\', '/')
        self.db_root_path = self.db_root_path.replace('\\', '/')

        self._component.path_to_root = self.db_root_path
        self._component.path_from_root = self._filepath

    def getComponent(self):
        # get the parsed/completed plant component
        return self._component
    component = property(getComponent)

    def _reset(self):
        self._docxtagpath = []
        self._docxtagpath.append('body')
        self._docxtagpath.append('tbl')
        self._docxtagpath.append('tr')
        self._nav_to_table = ''

        self._nav_in_table = {}
        self._nav_in_table['cell'] = 'tc'
        self._nav_in_table['paragraph'] = 'p'
        self._nav_in_table['run'] = 'r'
        self._nav_in_table['text'] = 't'
        self._nav_in_table['smarttag'] = 'smartTag'

        self._pic_count = 1
        self._rows = None
        self._component = Component()
        self._nonfatal_errors = []
        
    def getFilepath(self):
        # Get and set the filepath to the docx file that will be processed.
        return self._filepath
    def setFilepath(self, val):
        self._filepath = val
        self._reset()
    filepath = property(getFilepath, setFilepath)

    def getNonfatalErrors(self):
        return self._nonfatal_errors
    nonfatal_errors = property(getNonfatalErrors)

